两个TTL不一致如何制造一个必现的409
前言
线上有一个很奇怪的问题:OpenAI-compatible endpoint 第一次请求正常,放一段时间后再请求会稳定返回:
1 | 409 sampling run is not active |
再多等一会,它又可能自己恢复。
最初看起来像某个模型坏了,因为不同模型出现问题的时间并不一致;Trio 自己的 Sample API 当时又是正常的。继续复现后才发现,问题和模型没有关系,真正的规律是“同一个 API Key 和模型空闲超过 15 分钟”。
最后定位到的是一个很典型的分布式系统问题:Router 和 Actor 各自维护了一份生命周期,但两个 TTL 不一样。
请求链路
OpenAI 请求会经过:
1 | OpenAI SDK |
Router 为了避免每次请求都创建 ServiceSession 和 SamplingRun,会按 credential_id + model 缓存一份路由材料:
- ServiceSession ID
- SamplingRun ID
- Actor endpoint
- Actor JWT
- JWT 过期时间
Actor 则在本机维护 SamplingRun 的真实运行状态。一个 Run 长时间没有请求时,会被空闲回收。
单看两边的实现都很合理,放在一起就出问题了。
两个都合理的TTL
Router 当时用 Actor JWT 的剩余有效期判断缓存能否复用:
1 | JWT有效期:1小时 |
Actor 的 Run 空闲超时则是:
1 | run_idle_timeout:15分钟 |
于是同一份路由材料在两个服务眼里会有两种状态:
1 | 0~15分钟:Router认为有效,Actor也认为Run有效 |
这正好解释了“请求一开始成功,等十几分钟失败,再过很久又恢复”。
在 15~50 分钟这个窗口中,Router 会继续拿旧 SamplingRun ID 和旧 JWT 请求 Actor;Actor 查到 Run 已经不是 ACTIVE,于是返回 409。
完整修复
最小修复方案是让 Router Cache TTL 小于 Actor idle timeout,例如把缓存改成 10 分钟。
这能减少问题,但它把两个服务的内部配置耦合在了一起:
1 | Router必须知道Actor的回收时间 |
以后 Actor 调整 idle timeout,Router 也要同步修改。只要部署配置漂移,问题还会回来。
更稳妥的方式是让 Router 能识别“缓存指向的 Run 已经失效”,然后自愈。
最终实现的刷新流程是:
1 | 1. Router命中缓存并请求Actor |
这里必须“精确识别”,不能看到所有 409 就重试。参数冲突、业务状态冲突等 409 仍然应该原样返回,否则会把真实错误伪装成一次刷新。
也不能无限重试。旧 Run 失效属于一个确定、可恢复的前置条件错误,刷新一次足够;第二次还失败就应该结束请求。
缓存失效不是只删Redis
这次问题还有一个容易忽略的点:缓存 value 里不是普通查询结果,而是一组有生命周期的远程资源凭证。
删除 Redis key 只是本地动作,后续还要决定:
- 是否复用原 ServiceSession
- 是否需要关闭旧 Run
- 新 Run 创建失败时缓存保持什么状态
- 并发请求是否会重复创建 Run
- 原请求在什么条件下可以安全重放
因此 route cache 本质上并不只是性能缓存,它是一个分布式资源句柄缓存。命中不代表资源仍然存在,TTL 也不能代替服务端的真实状态。
最后
这次排查最有价值的地方,是把“偶发 409”还原成了一个确定的时间窗口。
分布式系统里,只要同一个资源在两个服务中各有一份生命周期,就不能只检查某一边的代码是否合理。50 分钟的凭证缓存和 15 分钟的资源空闲回收单独看都没错,组合起来却制造了一个 35 分钟宽的稳定故障窗口。
相关记录:
两个TTL不一致如何制造一个必现的409
https://blog.novashen.top/2026/07/10/tech/AI Infra/两个TTL不一致如何制造一个必现的409/